\documentclass{ctexart}
\usepackage{ctex}
\usepackage{cite}
\usepackage{float}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage[colorlinks,linkcolor=blue]{hyperref}
\usepackage{geometry}
\usepackage{hyperref}
\usepackage{amsfonts, amssymb}
\usepackage{url}
\usepackage{array}
\usepackage{xcolor}
\usepackage{listings}
\usepackage{fontspec}

\setmonofont{Consolas}
\linespread{1.5}
\pagestyle{plain}
\geometry{a4paper, scale=0.9}

\ctexset{
    section={
		format+ = \zihao{-3} \heiti \raggedright,
		name = {,、},
		number = \chinese{section},
		beforeskip = 1.0ex plus 0.2ex minus .2ex,
		afterskip = 1.0ex plus 0.2ex minus .2ex,
		aftername = \hspace{0pt}
    },
    % subsection={
	% 	format+ = \zihao{4} \kaishu \raggedright,
	% 	number = \chinese{subsection},
	% 	name = {（,）},
	% 	beforeskip = 1.0ex plus 0.2ex minus .2ex,
	% 	afterskip = 1.0ex plus 0.2ex minus .2ex,
	% 	aftername = \hspace{0pt}
	% },
	% subsubsection={
	% 	format+ = \zihao{-4} \fangsong \centering,
	% 	number = \ttfamily\arabic{subsubsection},
	% 	name = {,.},
	% 	beforeskip = 1.0ex plus 0.2ex minus .2ex,
	% 	afterskip = 1.0ex plus 0.2ex minus .2ex,
	% 	aftername = \hspace{0pt}
	% }
}

\lstset{
    basicstyle          =   \sffamily,           % 基本代码风格
    keywordstyle        =   \bfseries,           % 关键字风格
    commentstyle        =   \rmfamily\itshape,   % 注释的风格，斜体
    stringstyle         =   \ttfamily,           % 字符串风格
    flexiblecolumns,                             % 别问为什么，加上这个
    numbers             =   left,                % 行号的位置在左边
    showspaces          =   false,               % 是否显示空格，显示了有点乱，所以不现实了
    numberstyle         =   \zihao{-5}\ttfamily, % 行号的样式，小五号，tt等宽字体
    showstringspaces    =   false,
    captionpos          =   t,                   % 这段代码的名字所呈现的位置，t指的是top上面
    frame               =   lrtb,                % 显示边框
    language            =   [ANSI]{C},
    breaklines          =   true,
}

\definecolor{mygreen}{rgb}{0,0.6,0}
\definecolor{mygray}{rgb}{0.5,0.5,0.5}
\definecolor{mymauve}{rgb}{0.58,0,0.82}
\lstset{ %
        backgroundcolor=\color{white},   % choose the background color
        basicstyle=\footnotesize\ttfamily,        % size of fonts used for the code
        columns=fullflexible,
        breaklines=true,                 % automatic line breaking only at whitespace
        captionpos=b,                    % sets the caption-position to bottom
        tabsize=4,
        commentstyle=\color{mygreen},    % comment style
        escapeinside={\%*}{*)},          % if you want to add LaTeX within your code
        keywordstyle=\color{blue},       % keyword style
        stringstyle=\color{mymauve}\ttfamily,     % string literal style
        frame=single,
        rulesepcolor=\color{red!20!green!20!blue!20},
        % identifierstyle=\color{red},
        backgroundcolor=\color[RGB]{245,245,244},
        numberstyle=\color[RGB]{0,192,192},
        stringstyle=\rmfamily\slshape\color[RGB]{128,0,0},
        identifierstyle=\bf,
        language=C++,
}

\title{\huge \textbf{《项目经历，for实习、秋招》\\ 史煜鑫}}
\date{}

\begin{document}

    \begin{figure}
        \centering
        \vspace*{8\baselineskip}
        \includegraphics[scale=0.4]{Figures/封面2.png}
        \vspace*{4\baselineskip}
    \end{figure}
    
    \maketitle

    \begin{table}[htb]
        \LARGE
        \renewcommand\arraystretch{1.5}
        \begin{center}
            \begin{tabular}{ccc}
                最后编译日期： & \today \\ \cline{2-2}
            \end{tabular}
        \end{center}
    \end{table}

    \begin{center}
        本文档使用 \LaTeX{} 编写
    \end{center}

    \clearpage

    本文档，涵盖了在准备2024届实习、秋招的过程中，本人在项目经历方面，所做的工作。

    这个引用\cite{csapp}没有什么用，只是为了通过编译。

    \tableofcontents

    \clearpage
    \section{牛客网、C++、Linux高并发服务器开发}

    \href{https://www.nowcoder.com/courses/cover/live/504}{链接}

    国内会使用到C++的领域包括，Linux服务器开发（居多），桌面客户端开发，游戏开发，网络安全，音视频开发，渲染引擎，游戏引擎，嵌入式，金融等领域的数据处理。

    C++，Linux服务器，常见面试问题，进程间的通信方式、僵尸进程、孤儿进程、线程同步怎么解决、大端和小端、IO多路复用、静态库和共享库、滑动窗口机制、TCP的三次握手四次挥手、TCP与UDP的区别，……

    如果要深入的学习这些知识，可用的参考书目，《Unix环境高级编程》、《Linux/Unix系统编程手册》、《Linux内核设计与实践》、《计算机网络》、《TCP/IP详解卷》、《Unix网络编程》。

    \href{https://zhuanlan.zhihu.com/p/519732843}{Ubuntu20.04源码安装Cmake}。

    \href{https://blog.csdn.net/wyg1997/article/details/101460961}{VsCode SSH连接服务器失败}。

    \subsection{Linux系统编程入门}

    \subsubsection{G++基础知识}

    \begin{lstlisting}[language=C++, lineskip=3pt]
        Usage: g++ [options] file...
        Options:
          -pass-exit-codes         Exit with highest error code from a phase.
          --help                   Display this information.
          --target-help            Display target specific command line options.
          --help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...].
                                   Display specific types of command line options.
          (Use '-v --help' to display command line options of sub-processes).
          --version                Display compiler version information.
          -dumpspecs               Display all of the built in spec strings.
          -dumpversion             Display the version of the compiler.
          -dumpmachine             Display the compiler's target processor.
          -print-search-dirs       Display the directories in the compiler's search path.
          -print-libgcc-file-name  Display the name of the compiler's companion library.
          -print-file-name=<lib>   Display the full path to library <lib>.
          -print-prog-name=<prog>  Display the full path to compiler component <prog>.
          -print-multiarch         Display the target's normalized GNU triplet, used as
                                   a component in the library path.
          -print-multi-directory   Display the root directory for versions of libgcc.
          -print-multi-lib         Display the mapping between command line options and
                                   multiple library search directories.
          -print-multi-os-directory Display the relative path to OS libraries.
          -print-sysroot           Display the target libraries directory.
          -print-sysroot-headers-suffix Display the sysroot suffix used to find headers.
          -Wa,<options>            Pass comma-separated <options> on to the assembler.
          -Wp,<options>            Pass comma-separated <options> on to the preprocessor.
          -Wl,<options>            Pass comma-separated <options> on to the linker.
          -Xassembler <arg>        Pass <arg> on to the assembler.
          -Xpreprocessor <arg>     Pass <arg> on to the preprocessor.
          -Xlinker <arg>           Pass <arg> on to the linker.
          -save-temps              Do not delete intermediate files.
          -save-temps=<arg>        Do not delete intermediate files.
          -no-canonical-prefixes   Do not canonicalize paths when building relative
                                   prefixes to other gcc components.
          -pipe                    Use pipes rather than intermediate files.
          -time                    Time the execution of each subprocess.
          -specs=<file>            Override built-in specs with the contents of <file>.
          -std=<standard>          Assume that the input sources are for <standard>.
          --sysroot=<directory>    Use <directory> as the root directory for headers
                                   and libraries.
          -B <directory>           Add <directory> to the compiler's search paths.
          -v                       Display the programs invoked by the compiler.
          -###                     Like -v but options quoted and commands not executed.
          -E                       Preprocess only; do not compile, assemble or link.
          -S                       Compile only; do not assemble or link.
          -c                       Compile and assemble, but do not link.
          -o <file>                Place the output into <file>.
          -pie                     Create a dynamically linked position independent
                                   executable.
          -shared                  Create a shared library.
          -x <language>            Specify the language of the following input files.
                                   Permissible languages include: c c++ assembler none
                                   'none' means revert to the default behavior of
                                   guessing the language based on the file's extension.
        
        Options starting with -g, -f, -m, -O, -W, or --param are automatically
         passed on to the various sub-processes invoked by g++.  In order to pass
         other options on to these processes the -W<letter> options must be used.
        
        For bug reporting instructions, please see:
        <file:///usr/share/doc/gcc-9/README.Bugs>.
    \end{lstlisting}

    -E，进行预处理，-S，进行汇编，-c，转为二进制代码但是不进行链接，-D，再预处理时指定一个宏，-Wall，显示所有警告，-I，指明在哪里寻找头文件，-l，指定需要加载的库文件。

    \subsubsection{静态库和动态库的基础知识}

    静态库在链接阶段，被复制到程序体中，动态库在运行阶段动态加载。Linux命名静态库的规则为libXXX.a，Windows命名静态库的规则为libXXX.lib。

    Linux上，在使用GCC获取.o文件之后，可以使用ar rcs工具将.o文件打包进库文件。

    库文件和头文件要搭配在一起使用，GCC编译时，使用-l参数指定需要的库文件（不使用libXXX.a的全名，XXX即可），使用-L参数指定库文件所在的目录。

    Linux中，动态库的命名方式为libXXX.so，Windows中，动态库的命名为libXXX.dll，制作动态库分为两步，首先gcc -c -fpic，得到与位置无关的代码，然后gcc -shared得到动态库。

    -fpic 用于编译阶段，产生的代码没有绝对地址，全部用相对地址，这正好满足了共享库的要求，共享库被加载时地址不是固定的。如果不加-fpic ，那么生成的代码就会与位置有关，当进程使用该.so文件时都需要重定位，且会产生成该文件的副本，每个副本都不同，不同点取决于该文件代码段与数据段所映射内存的位置。

    动态库的使用与静态库无异（-l，-L参数），使用了动态库的程序，在加载时，会通过ldd（list dynamic dependency）命令检查动态库的依赖关系，然后，需要通过\textbf{动态载入器（ld-linux.so）}来获取所需动态库的绝对路径。

    \begin{lstlisting}[lineskip=3pt]
(base) shiyuxin@A-Server-for-Two:/sata1/shiyuxin-archive/codes/VSCodeCPPLinux$ ldd main
linux-vdso.so.1 (0x00007ffc0f743000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f603096e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f603077c000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f603062d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6030b6b000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f6030612000)
    \end{lstlisting}

    ld-linux.so，会依次搜索elf文件的，DT\_RPATH段，环境变量LD\_LIBRARY\_PATH，/etc/ld.so.cache文件列表，/lib/，/usr/lib，找到动态库后将其载入内存。

    可以使用export命令，临时修改环境变量:
    
    export LD\_LIBRARY\_PATH = \$LD\_LIBRARY\_PATH:NEW\_ENV

    多个进程可以共享一个加载到内存中的动态库。

    \subsubsection{编译动态库实战}

    文件结构：

    \begin{lstlisting}[lineskip=3pt]
.
├── app.cpp
├── include
│   └── calculation.h
├── lib
└── src
    ├── add.cpp
    └── sub.cpp
    \end{lstlisting}

    首先，通过\textbf{g++ -c -fpic add.cpp sub.cpp}命令，得到与位置无关的机器码（.o文件）。

    然后，通过\textbf{g++ -shared add.o sub.o -o libcal.so}命令，将.o文件打包为动态库，Linux中的动态库以.so作为后缀，文件名需要以lib开头。

    然后，通过\textbf{g++ app.cpp -I include/ -L lib/ -l cal -o app.out}命令，将程序文件（app.cpp），头文件，动态库文件，编译为可执行文件。其中，-I参数指定头文件的目录，-L指定动态库的目录，-l指定需要的动态库的名称（可以不需要前缀lib和后缀.so）。

    完成所有编译步骤后，文件结构：

    \begin{lstlisting}[lineskip=3pt]
.
├── app.cpp
├── app.out
├── include
│   └── calculation.h
├── lib
│   └── libcal.so
└── src
    ├── add.cpp
    ├── add.o
    ├── sub.cpp
    └── sub.o
    \end{lstlisting}

    在实际运行可执行文件时，需要让操作系统能够找到需要加载的动态库，常用的做法之一是，修改环境变量\textbf{LD-LIBRARY-PATH}，具体的命令是：
    
    \textbf{export LD\_LIBRARY\_PATH=\$LD\_LIBRARY\_PATH:home/shiyuxin/CPPServer/lib}
    
    然后，可以使用export命令查看修改是否生效，之后，就可以运行编译好的，app.out文件。

    \subsubsection{Makefile}

    Makefile的使用，可以实现自动化编译，只需要一个make命令就可以编译整个工程，make是一个工具，用来解析执行Makefile中的命令。

    Makefile的命名必须为“makefile”或者“Makefile”，一个Makefile文件由若干个\textbf{规则}组成，每个规则由\textbf{目标、依赖、命令}组成；其中，目标是最终需要生成的文件，依赖是生成目标需要的文件和目标，命令则是生成目标的具体命令（命令前必须有Tab缩进）。一般情况下，Makefile中的其他规则都是为第一条规则服务的。

    一个单个规则的Makefile如下，使用上面动态库的代码：

    \begin{lstlisting}[language=make, lineskip=3pt]
app: app.cpp
    g++ app.cpp -L lib/ -l cal -o app.out
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/shiyuxin/CPPServer/lib
    \end{lstlisting}

    Makefile在执行规则之前，会检查规则中的依赖是否存在，如果存在，就执行规则中的指令；如果不存在，就向下检查其他规则，检查其他规则是否有用来生成需要的依赖。

    Makefile中的cd命令，只在当前的行中生效。

    在Makefile中，对于每一行的命令，make会启动一个新的进程来执行命令，因此，直接使用系统的export命令来改变环境变量，无法在运行make命令的shell环境中生效。

    对于Makefile，如果想单独只执行Makefile中的某一个规则，可以make xx这样。

    使用上一节编译动态库中的代码作为示例，拥有多个规则的Makefile如下：

    \begin{lstlisting}[language=make, lineskip=3pt]
app.out: app.cpp src/add.o src/sub.o lib/libcal.so
    g++ app.cpp -L lib/ -l cal -o app.out
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/shiyuxin/CPPServer/lib && ./app.out

src/add.o: src/add.cpp
    cd src && g++ -c -fpic add.cpp -o add.o

src/sub.o: src/sub.cpp
    cd src && g++ -c -fpic sub.cpp -o sub.o

lib/libcal.so: src/add.o src/sub.o
    cd src && g++ -shared add.o sub.o -o libcal.so
    mv src/libcal.so lib/libcal.so

clean:
	rm src/add.o
	rm src/sub.o
	rm lib/libcal.so
    \end{lstlisting}

    每次make时，make会检查源码是否有修改，如果目标比依赖的时间戳更加新，就不会重复进行编译，如果发现依赖在目标上次生成之后发生过改动，就会重新编译。

    在Makefile中，使用xx=xx来\textbf{定义变量}，使用\$()来获取变量。Makefile中，有很多预定义的变量，例如，AR代表静态库打包工具，CC表示C编译器名称，CXX表示C++编译器名称，\$@表示目标的完整名称，\$<表示第一个依赖，\$\^{}表示所有的依赖。

    Makefile中还可以使用\textbf{通配符}，例如，使用\%来匹配相同的字符串，简化规则的编写。

    Makefile中，还支持\textbf{函数的使用}，调用格式是\$(函数名 \; 参数)，例如，wildcard函数，可以列出，某个目录下所有满足条件的文件的文件名，并以空格分隔；patsubst函数，可以执行批量字符串替换，函数的第一个参数是原始字符串模式，第二个参数是目标字符串模式，第三个参数是需要处理的字符串。

    使用了\textbf{自定义变量、通配符、函数}，的，Makrfile样例如下：

    \begin{lstlisting}[language=make, lineskip=3pt]
SRC=$(wildcard src/*.cpp)
OBJ=$(patsubst %.cpp, %.o, $(SRC))
DLL=cal
DLL_FULL_PATH=lib/libcal.so

app.out: app.cpp $(OBJ) $(DLL_FULL_PATH)
    echo $(SRC)
    echo $(OBJ)
    echo $(DLL)
    echo $(DLL_FULL_PATH)
    g++ app.cpp -L lib/ -l $(DLL) -o app.out
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/shiyuxin/CPPServer/lib && ./app.out

src/%.o: src/%.cpp
    g++ -c -fpic $< -o $@

$(DLL_FULL_PATH): $(OBJ)
    g++ -shared $(OBJ) -o $(DLL_FULL_PATH)

.PHONY:clean
clean:
    rm $(OBJ)   
    \end{lstlisting}

    像clean这样的规则，不需要生成什么文件，因此可以被标记为\textbf{伪目标}，使用.PHONY来完成。

    示例代码转移到了\textbf{/home/shiyuxin/CPPServer/01\_MakeFileTurtoial}目录下。

    \subsubsection{GDB调试}

    GDB的吉祥物是一条射水鱼。

    在需要进行调试时，一般会关闭编译器的优化（-Ox选项），同时打开调试开关（-g选项），并打开-Wall选项查看所有的Warning。

    编译器的-g选项的作用是，在可执行程序中，加入源代码的信息；例如可执行文件中的某一条机器指令对应源代码中的哪一行代码（并不会将源文件直接嵌入）；所以在使用GDB进行调试时，必须保证GDB可以找到源文件。

    \begin{itemize}
        \item 启动GDB：gdb 可执行程序；
        \item 退出GDB：quit，或者直接q；
        \item 设置/获取参数：set/show args；
        \item 获取帮助：help；
        \item 查看源码：list，或者直接l，后面可以跟需要查看的行号或者函数名称，或者文件名:行号、文件名:函数名称；
        \item 查看/设定一次性显示源代码的行数：set/show list/listsize。
        \item \textbf{设置断点}：break/b，后面可以跟行号、函数名、文件名：行号/函数名；
        \item \textbf{设置条件断点}：break 断点位置 if xx == xx；
        \item \textbf{查看断点}：i/info b/break；
        \item \textbf{disable断点}：dis/disable 断点编号；
        \item \textbf{enable断点}：ena/enable 断点编号；
        \item \textbf{删除断点}：d/del/delete 断点编号；
        \item \textbf{start}：程序运行，并停在第一行；
        \item \textbf{run}：程序运行，遇到断点才停止；
        \item \textbf{c/continue}：程序运行，直到下一个断点停止；
        \item \textbf{n/next}：向下执行一行代码，但是不会进入函数体；
        \item \textbf{s/step}：向下单步调试，遇到函数会进入函数体；
        \item \textbf{finish}：跳出函数体；
        \item \textbf{p/print 变量名}：打印变量的值；
        \item \textbf{ptype 变量名}：打印变量的类型；
        \item \textbf{until}：跳出循环；
        \item \textbf{set var 变量名 = 变量值}：调试过程中，设置变量值；
        \item \textbf{display xx}：设置程序在遇到断点时，自动打印某些变量的值；
        \item \textbf{i/info display}：查看所有设置了自动打印的变量；
        \item \textbf{undisplay 编号}：取消自动打印；
    \end{itemize}

    这次的示例代码在\textbf{/home/shiyuxin/CPPServer/02\_GDBTurtoial}目录下。

    当GDB在断点处停住时，断点所处的行并没有被执行；

    查看cal\_add和cal\_sub文件中的代码：\textbf{list cal\_add.cpp:add}、\textbf{list cal\_sub.cpp:sub}；

    在函数中设置断点：\textbf{break cal\_add.cpp:2}、\textbf{break cal\_sub.cpp:2}；

    查看已经设置的断点：\textbf{info break}；

    \begin{lstlisting}[lineskip=3pt]
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000000151b in add(double, double) at cal_add.cpp:2
2       breakpoint     keep y   0x0000000000001543 in sub(double, double) at cal_sub.cpp:2
    \end{lstlisting}

    \subsubsection{C语言标准库IO函数，和，Linux系统IO函数}

    这里的IO是从内存的角度来说的，从文件中读取数据到内存中是I，从内存中写数据到文件中是O。

    标准的C库IO函数，包括，fopen、fclose、fread、fwrite、fgets、fputs、fscanf、fprintf、fflush，等；标准C库是跨平台的。在Linux平台上，标准C库IO函数，通过调用Linux的系统API来实现。

    C语言标准库IO函数，通过实现缓冲区，实现了相比Linux系统IO函数更高的效率。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.7]{Figures/标准C库IO函数.png}
        \caption{C语言标准库IO函数}
    \end{figure}

    可以使用\textbf{man 3 fopen}命令，来查看C语言标准库中的fopen函数的使用手册。

    \textbf{文件描述符}指向一个已经打开的文件（Windows中，会称文件描述符为句柄）。

    在网络通信时，一般使用Linux系统IO函数，避免IO缓冲区造成的延迟。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.7]{Figures/虚拟地址空间.png}
        \caption{虚拟地址空间}
    \end{figure}

    虚拟地址空间是对物理内存和虚拟内存的抽象，每个应用程序都能看到一个虚拟地址空间，以0起始。内存管理单元MMU负责虚拟地址和物理地址之间的映射。

    PCB，进程控制块，位于虚拟地址空间的内核区，文件描述符由进程控制块进行管理。每个进程默认都有，标准输入、标准输出、标准错误，三个文件描述符（占用了012）。在打开文件时，会分配可用的最小的文件描述符。

    可以使用\textbf{man 2 open}来查看Linux系统IO函数的使用说明。

    \begin{lstlisting}[language=C, lineskip=3pt]
int open(const char *pathname, int flags);  // man 2 open
int open(const char *pathname, int flags, mode_t mode);  // man 2 open

open (const char *__path, int __oflag, ...) // /usr/include/x86_64-linux-gnu/bits/fcntl2.h
    \end{lstlisting}

    Linux系统IO函数中的open函数，通过C语言的可变参数而不是重载来实现。

    open函数会返回文件描述符（int类型），如果返回的文件描述符为-1，表示遇到了错误，具体的错误类型可以查看全局变量\textbf{errno}，errno是Linux系统函数中的一个全局变量，记录了最近的错误号。

    C语言标准库函数中，提供了一个函数，perror，可以打印errno对应的错误信息，位于stdio.h/cstdio头文件中。

    Linux系统IO提供的close能够根据文件描述符来关闭文件，成功关闭返回0，关闭失败返回1，同样可以通过errno查看错误编号，并通过perror打印具体的错误信息。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <iostream>
#include <cstdio>

// 使用Linux系统IO中的open函数，需要以下三个文件
#include <sys/types.h> // 使用过程中需要的宏定义
#include <sys/stat.h> // 使用过程中需要的宏定义
#include <fcntl.h> // 函数声明在这里

// 使用Linux系统IO中的close函数，需要以下头文件
#include <unistd.h>

using namespace std;

int main(int argc, char* argv[]) {
    cout << "This is WSL" << endl;

    // 打开一个已经存在的文件
    const char file_path[] = "./data.txt";
    int file_descriptor = open(file_path, O_RDONLY);

    // 查看是否成功打开
    if (file_descriptor == -1) {
        cout << "打开文件 " << file_path << " 失败！！！" << endl;
        cout << "errno = " << errno << endl;
        perror(file_path); // 打印对应的错误信息
    } else {
        cout << "文件描述符为： " << file_descriptor << endl;
    }

    // 通过文件描述符关闭文件
    int res = close(file_descriptor);

    // 查看是否成功关闭
    if (res == -1) {
        cout << "关闭文件 " << file_path << " 失败！！！" << endl;
        cout << "errno = " << errno << endl;
        perror(file_path);
    } else {
        cout << res << " 文件成功关闭" << endl;
    }
}
    \end{lstlisting}

    如果需要创新新的文件，则需要第三个参数，mode，这个参数会指明用户对open函数新创建的文件的权限。在输入这个参数时，一般会输入一个八进制的整数（C语言中，二进制数字使用0b或者0B开头，八进制的数字使用0开头，十六进制的数字使用0x或者0X开头），例如0777。

    r：可读；

    w：可写；

    x：可执行；

    当前用户对这个文件的权限，当前用户组对这个文件的权限，其他用户组的用户对这个文件的权限。

    最高的权限为0777，即，rwxrwxrwx。

    实际在open函数中，我们输入的mode，会经过 $ mode \& ~umask $ 的操作，作为创建的文件的最终权限，umsak代表新建文件的默认权限（默认权限为0777 - umask），可以直接使用umsak命令查看（root用户是0022，普通用户是0002）。

    通过上一步操作，可以抹去我们使用open函数创建的文件的某些权限，使得我们创建的文件的权限更加合理。

    在Linux系统API中，也有一个umask函数，可以设置umask的值，\textbf{man 2 umask}。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

int main(int argc, char* argv[]) {
    const char file_path[] = "./test.txt";
    int file_descriptor = open(file_path, O_WRONLY | O_CREAT, 0777);

    cout << "File Descriptor is " << file_descriptor << endl;
    perror(file_path);

    cout << close(file_descriptor) << endl;
}
    \end{lstlisting}

    Linux标准IO中的read函数和write函数，\textbf{man 2 read}，\textbf{man 2 write}，需要的头文件均为unistd.h。

    read函数，从文件描述符指定的文件中读取数据，存放在buf中，读取count个字节，返回值是实际读取的字节数，如果返回0，则表示文件读取完毕，如果返回-1，则表示文件读取出错。

    write函数，向文件描述符指定的文件中写入数据，要写入的数据在buf中，写入count个字节，返回值是实际写入的字节数，如果返回-1则表示文件写入失败。

    read函数和write函数，如果出现失败，都会设置errno，都可以通过perror打印错误信息。

    \begin{lstlisting}[language=C, lineskip=3pt]
ssize_t read(int fd, void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
    \end{lstlisting}

    使用，open，read，write，close，实现文件拷贝：

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <iostream>
#include <cstdio>

// 使用Linux系统IO中的open函数，需要以下三个文件
#include <sys/types.h> // 使用过程中需要的宏定义
#include <sys/stat.h> // 使用过程中需要的宏定义
#include <fcntl.h> // 函数声明在这里

// 使用Linux系统IO中的close，read，write函数，需要以下头文件
#include <unistd.h>

using namespace std;

int main(int argc, char* argv[]) {
    cout << "This is WSL" << endl;

    // 打开需要读取的文件
    const char source_file_path[] = "./source.txt";
    int source_file_descriptor = open(source_file_path, O_RDONLY);

    if (source_file_descriptor == -1) {
        cout << "Error Opening " << source_file_path << endl;
        cout << "Errno = " << errno << endl;
        perror(source_file_path);
        return -1;
    }
    
    // 创建需要写入的文件
    const char destination_file_path[] = "./destination.txt";
    int destination_file_descriptor = open(destination_file_path, O_WRONLY | O_CREAT, 0777);

    if (destination_file_descriptor == -1) {
        cout << "Error Creating " << destination_file_path << endl;
        cout << "Errno = " << errno << endl;
        perror(destination_file_path);
        return -1;
    }

    // 创建缓冲区
    const int buffer_size = 256;
    char buffer[buffer_size + 1];

    while(true) {
        // 读取数据
        int read_size = read(source_file_descriptor, buffer, buffer_size);

        // 处理读取错误
        if (read_size == -1) {
            cout << "Reading Error " << source_file_path << endl;
            perror(source_file_path);
            return -1;
        } else {
            buffer[read_size] = '\0';
            cout << buffer;
        }

        // 写入读取的数据
        int write_size = write(destination_file_descriptor, buffer, read_size);

        // 处理写入错误
        if (write_size == -1) {
            cout << "Writing Error " << destination_file_path << endl;
            perror(destination_file_path);
            return -1;
        }

        // 拷贝完成
        if (read_size == 0) break;
    }

    // 关闭文件
    close(source_file_descriptor);
    close(destination_file_descriptor);

    cout << endl;
    cout << "--------------------" << endl;
    cout << "Done File Copy" << endl;
    cout << "--------------------" << endl;
}        
    \end{lstlisting}

    \textbf{man 2 lseek}，lseek函数用于重新定位文件的读写指针，第一个参数为文件描述符，第二个参数为偏移量，第三个参数可以为，SEEK\_SET、SEEK\_CUR、SEEK\_END，之一，表示从文件的开头、当前读写指针的位置、文件的末尾，开始计算偏移，偏移量可以为正也可以为负。函数的返回值为移动后的文件指针位置。

    lseek(fd, 0, SEEK\_SET)可以将读写指针移动到文件的开头。

    lseek(fd, 0, SEEK\_CUR)可以获取当前读写指针的位置。

    lseek(fd, 0, SEEK\_END)可以获取文件的长度。

    lseek(fd, 23333, SEEK\_END)可以拓展文件的长度（需要写入数据生效）。

    \begin{lstlisting}[language=C, lineskip=3pt]
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
    \end{lstlisting}

    使用lseek拓展文件长度并追加内容的代码如下：

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <iostream>
#include <cstdio>
#include <cstring>

// 使用Linux系统IO中的open函数，需要以下三个文件
#include <sys/types.h> // 使用过程中需要的宏定义
#include <sys/stat.h> // 使用过程中需要的宏定义
#include <fcntl.h> // 函数声明在这里

// 使用Linux系统IO中的close，read，write函数，需要以下头文件
#include <unistd.h>

using namespace std;

int main(int argc, char* argv[]) {
    cout << "This is WSL" << endl;

    // 打开文件
    int file_descriptor = open("./source.txt", O_WRONLY);
    
    // 读写指针移动到文件末尾
    lseek(file_descriptor, 0, SEEK_END);

    // 拓展文件长度
    lseek(file_descriptor, 100, SEEK_CUR);

    // 写入新的数据
    char new_line[] = "\nThis is 史煜鑫\n";
    write(file_descriptor, new_line, strlen(new_line));

    // 关闭文件
    close(file_descriptor);
}
    \end{lstlisting}

    \textbf{man 2 stat/lstat}，这两个函数用于返回文件的信息，第一个参数是文件路径，第二个是一个stat结构体指针。成功获取文件信息则返回0，否则返回-1，并设置errno。

    Linux中，有7种文件类型，普通文件、目录、链接文件、块设备、字符设备、管道文件、套接字文件。

    C语言中，结构体可以和函数重名，在初始化结构体时，重名的结构体需要\textbf{struct xx a}这样来创建。

    \begin{lstlisting}[language=C, lineskip=3pt]
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
    \end{lstlisting}

    \begin{lstlisting}[lineskip=3pt]
shiyuxin@史煜鑫的ConceptD3-Ezel:~/CPPServer/03_LinuxIO$ stat ./source.txt 
File: ./source.txt
Size: 2268            Blocks: 8          IO Block: 4096   regular file
Device: 810h/2064d      Inode: 36411       Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/shiyuxin)   Gid: ( 1000/shiyuxin)
Access: 2023-06-17 11:43:49.349560700 +0800
Modify: 2023-06-17 11:43:49.329560700 +0800
Change: 2023-06-17 11:43:49.329560700 +0800
Birth: 2023-06-16 09:47:51.052272900 +0800
    \end{lstlisting}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.9]{Figures/stat结构体.png}
        \caption{stat结构体}
    \end{figure}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.7]{Figures/st mode变量.png}
        \caption{st mode 变量}
    \end{figure}

    使用stat或者lstat函数，获取文件属性的示例如下：

    这里还用到了一个函数ctime，\textbf{man 3 ctime}。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>

// 使用Linux系统IO中的open函数，需要以下三个文件
#include <sys/types.h> // 使用过程中需要的宏定义
#include <sys/stat.h> // 使用过程中需要的宏定义
#include <fcntl.h> // 函数声明在这里

// 使用Linux系统IO中的close，read，write函数，需要以下头文件
#include <unistd.h>

using namespace std;


int main(int argc, char* argv[]) {
    cout << "This is WSL" << endl;

    struct stat buffer;
    int res = stat("./source.txt", &buffer);

    if (res == -1) {
        cout << "Error stat ./source.txt" << endl;
        cout << "errno = " << errno << endl;
        perror("./source.txt");
    }

    cout << "设别编号：" << buffer.st_dev << endl;
    cout << "文件大小：" << buffer.st_size << endl;
    cout << "最后一次访问：" << ctime(&buffer.st_atime);
    cout << "最后一次modify：" << ctime(&buffer.st_mtime);
    cout << "最后一次change：" << ctime(&buffer.st_ctime);
}
/*
shiyuxin@史煜鑫的ConceptD3-Ezel:~/CPPServer/03_LinuxIO$ make
g++ -Wall app.cpp -o app
shiyuxin@史煜鑫的ConceptD3-Ezel:~/CPPServer/03_LinuxIO$ ./app
This is WSL
设别编号：2064
文件大小：2268
最后一次访问：Sat Jun 17 11:43:49 2023
最后一次modify：Sat Jun 17 11:43:49 2023
最后一次change：Sat Jun 17 11:43:49 2023
*/     
    \end{lstlisting}

    与文件属性操作相关的函数有，\textbf{access、chmod、chown、truncate，}等。access可以判断文件是否存在，或者判断文件的权限。chmod（change mode）用于修改文件的权限，chwon（change owner）用于修改文件的所有者，或者文件的所在组。truncate则用来修改文件的大小。可以使用man 2 xx来查看具体的用法。

    access函数的mode参数，可以为R\_OK（判断是否存在），R\_OK（判断是否可读），W\_OK（判断是否可写），X\_OK（判断是否可执行）。成功返回0，失败返回-1。

    chmod的mode参数，可以自己指定一个八进制的数，也可以使用预定义过的宏。成功返回0，失败返回-1。

    在使用chown函数时，可以通过/etc/passwd文件，或者/etc/group文件，查看用户和组的id。

    \begin{lstlisting}[language=C, lineskip=3pt]
#include <unistd.h>
int access(const char *pathname, int mode);

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
    \end{lstlisting}

    与操作目录相关的函数有，mkdir、rmdir、rename、chdir、getcwd，等，mkdir用于新建文件夹。rmdir用于删除空的文件夹。rename用于重命名或者复制移动文件或者文件夹。chdir用于更改当前进程的工作目录。getcwd用于获取当前的工作目录。

    \begin{lstlisting}[language=C, lineskip=3pt]
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);

#include <unistd.h>
int rmdir(const char *pathname);

#include <stdio.h>
int rename(const char *oldpath, const char *newpath);

#include <unistd.h>
int chdir(const char *path);

#include <unistd.h>
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
    \end{lstlisting}

    Linux系统IO函数中，与遍历目录有关的函数是，opendir、readdir、closedir，可以使用man 3查看使用说明。

    opendir函数会打开一个目录流（directory stream），如果没有成功打开目录会返回空指针并设置errno。

    readdir函数会根据目录流读取一个目录，每次返回目录中的一个entry，如果读完了整个文件夹，就会返回NULL并且errno不变，如果读取出现了错误就会返回NULL并设置error。

    closedir用于关闭一个目录流，成功返回0，失败返回-1并设置errno。

    \begin{lstlisting}[language=C, lineskip=3pt]
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);

#include <dirent.h>
struct dirent *readdir(DIR *dirp);

struct dirent {
    ino_t          d_ino;       /* Inode number */
    off_t          d_off;       /* Not an offset; see below */
    unsigned short d_reclen;    /* Length of this record */
    unsigned char  d_type;      /* Type of file; not supported by all filesystem types */
    char           d_name[256]; /* Null-terminated filename */
};

#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
    \end{lstlisting}

    使用opendir、readdir、closedir，去读一个文件夹下所有文件的示例，如下：

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>

// 使用Linux系统IO中的open函数，需要以下三个文件
#include <sys/types.h> // 使用过程中需要的宏定义
#include <sys/stat.h> // 使用过程中需要的宏定义
#include <fcntl.h> // 函数声明在这里

// 使用Linux系统IO中的close，read，write，等函数，需要以下头文件
#include <unistd.h>

// open、read、close文件夹需要的头文件
#include <dirent.h>

using namespace std;

int main(int argc, char* argv[]) {
    const char dir_path[] = "/home/shiyuxin/CPPServer/01_MakeFileTurtoial";

    DIR* dir = opendir(dir_path);

    if (dir == nullptr) {
        cout << "Error Opening " << dir_path << endl;
        cout << "errno = " << errno << endl;
        perror(dir_path);
    }

    while (true) {
        dirent* item = readdir(dir);

        if (item == nullptr && errno != 0) {
            cout << "Error Reading Directory " << dir_path << "errno = " << errno << endl;
            perror(dir_path);
            break;
        }

        if (item == nullptr && errno == 0) {
            cout << "Done Reading " << dir_path << endl;
            break;
        }

        cout << dir_path << "/" << item->d_name << endl;
        switch (item->d_type) {
            case DT_BLK:
                cout << "是一个块设备" << endl;
                break;
            case DT_CHR:
                cout << "是一个字符设备" << endl;
                break;
            case DT_DIR:
                cout << "是一个目录" << endl;
                break;
            case DT_LNK:
                cout << "是一个链接文件" << endl;
                break;
            case DT_FIFO:
                cout << "是一个管道" << endl;
                break;
            case DT_REG:
                cout << "是一个普通文件" << endl;
                break;
            case DT_SOCK:
                cout << "是一个套接字文件" << endl;
                break;
            case DT_UNKNOWN:
                cout << "未知的文件类型" << endl;
                break;
        }
        cout << endl;
    }
    

    if (closedir(dir) == -1) {
        cout << "Error Closing " << dir_path << endl;
        cout << "errno = " << errno << endl;
        perror(dir_path);
    }
}        
    \end{lstlisting}

    Linux标准IO函数中，dup函数用于复制文件描述符，dup2函数用于重定向文件描述符。

    \begin{lstlisting}[language=C, lineskip=3pt]
// man 2 dup
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
    \end{lstlisting}

    fcntl函数可以对文件描述符进行多种操作，fcntl可以复制文件描述符，也可以设置/获取文件描述符的状态（获取open文件时，设置的flags，或者重新设置文件的flags）。

    \begin{lstlisting}[language=C, lineskip=3pt]、
// man 2 fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
    \end{lstlisting}

    \paragraph{总结}~{}

    open函数打开/创建文件，close函数关闭文件，read函数读取文件，write函数写入文件。

    lseek用于重定位文件的读写指针，通过重定位读写指针的方式也可以拓展文件的长度。

    stat/lstat函数可以获取文件的详细信息。

    access函数可以判断文件是否存在，或者判断文件的权限。chmod函数用于修改文件的权限。chown函数用于修改文件的所有用户或者用户组。truncate函数用于更改文件的大小。

    mkdir函数用于新建文件夹。rmdir用于删除空的文件夹。rename函数用于重命名或者复制移动文件或者文件夹。chdir函数用于更改当前进程的工作目录。getcwd用于获取当前的工作目录。

    opendir函数可以打开一个目录。readdir函数可以读取目录中的文件和目录。closedir函数可以关闭一个目录。

    dup函数用于复制文件描述符，dup2函数用于重定向文件描述符。

    fcntl函数有很多功能，可以复制文件描述符，也可以查询/设置文件描述符的状态（使用open函数时的flags）。

    \clearpage
    \subsection{Linux多进程开发}

    \subsubsection{待定}

    \subsection{Linux多线程开发}

    \subsubsection{待定}

    \subsection{Linux网络编程}

    \subsubsection{MAC、IP、Port、协议}

    网卡\&MAC地址（Media Access Control Address），属于数据链路层，主要功能是，数据的封装和解封装、链路管理、数据编码与译码，MAC地址长度为6个字节28为，通常用16进制表示为xx-xx-xx-xx-xx-xx，前三个字节用来标识制造商，由IEEE统一分配，后三个字节由设备制造商自己决定。理论上，每一张网卡的MAC地址是唯一的。

    Linux上，可以使用ifconfig工具，查看计算机上所有的网卡和MAC地址。

    点分十进制。

    \begin{itemize}
        \item 0.0.0.0，代表当前主机；
        \item 255.255.255.255，代表当前子网的广播地址；
        \item 127开头的地址用于贿赂测试，127.0.0.1到127.255.255.255，都用于回路测试，127.0.0.1可以代表本机localhost；
    \end{itemize}

    \begin{itemize}
        \item 周知端口，Wel-Known Ports，从0到1023，
        \item 注册端口，Registered Ports，1024到49151，用于松散的绑定一些服务，
        \item 动态端口/私有端口，Dynamic Ports/Private Ports，不固定分配某种服务，动态分配，
    \end{itemize}

    使用netstat -anp命令，可以查看进程使用的IP和端口号；

    各种通信协议，最终的体现就是，在网络上传输的各种数据包的格式；

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.4]{Figures/UDP图解.png}
        \caption{UDP协议图解}
    \end{figure}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.4]{Figures/TCP图解.png}
        \caption{TCP协议图解}
    \end{figure}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.4]{Figures/IP图解.png}
        \caption{IP协议图解}
    \end{figure}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.5]{Figures/通信协议封装.png}
        \caption{通信协议是如何一层一层封装的}
    \end{figure}

    ARP地址：根据IP地址查找MAC地址；

    RARP协议：根据MAC地址查找IP地址；

    Socket用于不同主机上两个进程间的通信；

    大端法：高位字节放在内存地址的低处。小端法：高位字节放在内存地址的高处；

    大部分X86的机器和ARM的机器，都是小端的；在网络上传输的数据，都是大端的；

    \subsubsection{字节序转换函数}

    大部分X86的机器和ARM的机器，都是小端的；在网络上传输的数据，都是大端的；

    字节序转换函数，包括了，htons，ntohs，htonl，ntohl，（net，host），用于网络和主机之间的，字节序的转换，主要用于IP和Port的转换。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
    \end{lstlisting}

    \subsubsection{Socket、IP、TCP}

    Linux下，Socket地址是结构体sockaddr，是对IP和端口号的封装，

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <bits/socket.h>

struct sockaddr
{
__SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
char sa_data[14];		/* Address data.  */
};

#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

typedef unsigned short int sa_family_t;
    \end{lstlisting}

    sa family变量用来表示地址族，与协议族相对应，比如地址族AF INET对应协议族PF INET，即TCP/IPv4协议。

    sa data变量，则用来存储具体的地址的值。

    如果协议所使用的地址比14字节更长，可以使用sockaddr storage结构体。

    sockaddr结构体，诞生的时间很早了，现在使用很不方便，主要起到一个向前兼容的作用，sockaddr指针现在一般被当作void指针来使用；实际Socket编程中，会使用的地址结构体是，sockaddr in（对应IPv4）、sockaddr in6（对应IPv6），等。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <netinet/in.h>

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port;			/* Port number.  */
struct in_addr sin_addr;		/* Internet address.  */

/* Pad to size of `struct sockaddr'.  */
unsigned char sin_zero[sizeof (struct sockaddr)
            - __SOCKADDR_COMMON_SIZE
            - sizeof (in_port_t)
            - sizeof (struct in_addr)];
};

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};
    \end{lstlisting}

    在表示IP地址时，为了可读性，IPv4地址会采用点分十进制来表示，IPv6地址会采用十六进制字符串来表示。在编程时，会涉及到IP地址的字符串和二进制整数之间的转换。

    inet addr函数，可以将字符串形式的IP地址，转换为网络字节序的整数形式。pton和ntop这两个函数更加新，支持IPv6。成功转换返回1，地址非法返回0，出现错误返回-1。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    \end{lstlisting}

    示例：

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <cstdio>
#include <iostream>

#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;

int main(int argc, char* argv[]) {

    char ip_addr_str[16] = "127.0.0.1";
    unsigned int ip_addr_int = 0;

    int res1 = inet_pton(AF_INET, ip_addr_str, &ip_addr_int);

    if (res1 == 0) {
        cerr << "不合法的IP地址：" << ip_addr_str << endl;
        return -1;
    } else if (res1 == -1) {
        cerr << "转换IP地址出现错误，errno = " << errno << endl;
        perror("IP地址转换出错，");
        return -1;
    }

    cout << "字符串形式：" << ip_addr_str << endl;
    cout << "网络大端字节序形式：" << hex << ip_addr_int << endl;

    const char* res2 = inet_ntop(AF_INET, &ip_addr_int, ip_addr_str, sizeof(ip_addr_str));

    if (res2 == nullptr) {
        cerr << "转换IP地址出错" << endl;
    }

    cout << "转换回字符串：" << ip_addr_str << endl;

    return 0;
}
    \end{lstlisting}

    使用Socket进行一次完整的TCP通信，会使用到的Linux网络通信函数如下所示。
    
    服务器会新建一个Socket，然后绑定到本地的IP和端口号上，然后设置为监听，然后让Socket进入阻塞等待客户端连接的状态，如果有客户端连接进来，会得到一个和客户端通信的Socket文件描述符，然后进行数据收发，然后关闭连接。

    客户端会新建一个Socket，不用绑定IP和端口号，然后根据服务器的IP和端口连接到服务器，连接成功之后开始通信收发数据，通信完成后断开连接。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.5]{Figures/TCP通信过程.png}
        \caption{TCP通信过程需要使用到的函数}
    \end{figure}

    在Linux中，创建一个Socket，使用socket函数进行。domain用于指明使用的协议簇（AF INET(6)），type参数指明socket的类型，最常用的是TCP的STREAM类型，流式，和，UDP的DGRAM类型，报式，protocol参数在type参数的基础之上指明使用的具体协议，protocol参数一般为0。当socket创建成功时，返回对应的文件描述符，失败则返回-1并设置errno。

    将创建好的Socket，绑定到指定的IP和端口号上，使用bind函数进行。第一个参数是socket的文件描述符，第二个参数是socket地址，第三个参数则指明第二个参数占用内存的大小（因为sockaddr*现在基本当成viod*来用）。绑定成功返回0，绑定失败返回-1，并设置errno。

    监听一个Socket，使用listen函数，listen函数的第一个参数是用于监听的Socket的文件描述符，第二个参数的含义是，已经建立的连接和等待建立的连接的最大数量。对于同时可以存在的最大的Socket连接的数量的系统设置，保存在/proc/sys/net/core/somaxconn中，本机为4096。listen函数调用成功返回0，调用失败返回-1，并设置errno。

    服务器端接收客户端的Socket连接，使用accept函数，默认情况下，accept是一个阻塞的函数。第一个参数是服务器的Socket文件描述符，第二个参数和第三个参数用于记录连接成功后，客户端的地址信息（IP+端口）。当accept函数调用成功时，会返回用于和客户端进行通信的文件描述符，当调用失败时，会返回-1，并设置errno。

    客户端连接服务器，使用connect函数，第一个参数是Socket的文件描述符，第二个参数和第三个参数用于传递服务器的地址信息。connect成功，返回0，失败则返回-1，并设置errno。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    \end{lstlisting}

    \subsubsection{IO多路复用/多路转接}

    IO多路复用技术，能够让程序一次性监听多个文件描述符，从而提高程序的性能，Linux环境下，实现IO多路复用，主要有select、poll、epoll三种方式。这里的IO指的是内存与文件之间。

    select、poll、epoll的基本原理是，内核代替程序来检查一系列IO是否就绪。select和poll只能通知程序是否有数据到达，epoll能通知程序具体有哪几个数据到达了。

    \paragraph{Select}~{}

    Select的设计思路如下：
    
    \begin{enumerate}
        \item 构建一个文件描述符列表，将需要进行监听的文件描述符放到这个列表中。
        \item 调用系统函数，监听列表中的文件描述符，当列表中的文件描述符有一个或者多个需要进行IO操作时，函数返回（函数是阻塞的，检查每个文件描述符的操作由操作系统内核完成）。
        \item 函数在返回时，通知用户进程，多少/哪些文件描述符上需要IO操作。
    \end{enumerate}

    select函数的参数的意义如下：

    \begin{enumerate}
        \item nfds：委托内核进行检测的文件描述符的最大值 + 1。
        \item readfds：要检测读事件的文件描述符的集合。函数对这个参数指示的文件描述符检测读事件，也通过这个参数返回哪些文件描述符发生了读事件。是一个传入传出参数。
        \item writefds：要检测写事件的文件描述符的集合。同样是一个传入传出参数。检测指示出来的文件描述符里，哪些文件描述符的写缓冲区还可以继续写。
        \item exceptfds：要检测的发生异常的文件描述符的集合。
        \item timeout：超时时间设置。如果为null，则表示永久阻塞。如果为0，表示不阻塞。
    \end{enumerate}

    select在发生错误调用失败时，返回-1，并设置errno。如果调用成功，返回三个集合（读、写、异常）中，发生了变化的文件描述符的个数。

    fd set类型，相当于一个128个字节，1024位的整数，用二进制掩码的方式来实现文件描述符集合。

    FD CLR函数，用于将指定的标志位，设置为0。FD ISSET函数，用于判断指定的标志位是0还是1。FD SET函数，用于将指定的标志位设置为1。FD ZERO函数，用于清空所有标志位。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
    \end{lstlisting}

    \paragraph{Poll}~{}

    对于select来说，将fds从用户态拷贝到内核态需要一定的资源，而且要遍历到第一个参数指定的文件描述符最大值。并且，select支持的文件描述符数量太小了，文件描述符集合每次也需要重置。

    poll将文件描述符封装为了结构体，以数组形式传递文件描述符。timeout参数如果为0，则表示不阻塞，如果为-1，则表示阻塞至有文件描述符发生变化，如果>0，则表示阻塞时长（单位为ms）。

    poll如果调用失败，返回-1，并设置errno。如果调用成功，则返回发生变化的文件描述符的个数，如果返回0，则表示timeout超时。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <poll.h>

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    \end{lstlisting}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.6]{Figures/Poll events.png}
        \caption{Poll中的各种事件}
    \end{figure}

    \paragraph{Epoll}~{}

    poll的缺点是，仍然需要将需要检测的文件描述符集合从用户态拷贝至内核态，仍然需要顺序遍历（包括内核检测和应用程序处理）。

    epoll create函数可以创建一个epoll实例，这个实例存在于内核区，epoll实例以红黑树的结构存放需要监测的文件描述符。

    epoll ctl函数用于向epoll实例中，添加、删除或者修改，文件描述符要检测的事件。

    epoll wait函数用于等待文件描述符上发生事件。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/epoll.h>

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;
    \end{lstlisting}

    \subsection{项目实战与总结}

    \subsubsection{阻塞，非阻塞，异步，同步}

    一次典型的IO分为，数据就绪，和，数据读写，两个阶段；

    数据就绪，根据系统IO的操作状态，可以分为阻塞额非阻塞；

    数据读写，根据应用程序和内存交互的方式，可以分为同步和异步；

    \begin{itemize}
        \item \textbf{阻塞}，在数据到达前，调用IO方法的线程/进程处于挂起状态。
        \item \textbf{非阻塞}，在数据到达钱，调用IO方法的线程/进程，不会被挂起，状态不被改变。
        \item \textbf{同步}，数据在操作系统内核中准备完成后，应用程序主动从内核中读取数据，数据转移过程中应用程序不会进行其他操作。同步的重点在于，数据在操作系统内核中准备好后，读取是应用程序自己完成的。同步的效率相对较低，但是编写起来简单。
        \item \textbf{异步}，异步编程比较繁琐，需要操作系统支持异步的接口。异步读取数据，需要将指定的缓冲区传递给操作系统，操作系统负责向缓冲区中写入数据，写入完成后操作系统发出通知通知应用程序。
    \end{itemize}

    \textbf{在处理IO的时候，阻塞和非阻塞都是同步IO，只有使用了特殊API的才是异步IO。}

    Linux下，异步使用AIO；Windows下，异步使用IOCP；.Net中，异步使用BeginInvoke/EndInvoke。

    一个典型的网络IO调用，分为两个阶段，分别是\textbf{数据就绪}和\textbf{数据读写}，数据就绪阶段分为\textbf{阻塞和非阻塞}，表现的结果就是，阻塞当前线程或是直接返回。

    同步表示A向B请求调用一个网络IO接口时，或者调用某个业务逻辑API接口时，数据的读写都是由请求方A自己来完成的，不管是阻塞还是非阻塞。

    异步表示A向B请求调用一个网络接口时，或者调用某个业务逻辑API接口时，向B传入请求的事件和事件发生时的通知方式，A接下来就可以处理其他逻辑了，当B监听到事件处理完成后，就会用事先约定好的通知方式，通知A处理结果。

    ---

    IO多路复用，select、poll、epoll，IO多路复用都是同步的。

    \subsubsection{Unix/Linux上的5种IO模型}

    \paragraph{1、阻塞，Blocking}~{}

    调用者调用某个函数，并等待这个函数返回，等待期间不进行任何操作，直到函数返回后在进行下一步操作。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.9]{Figures/阻塞IO.png}
        \caption{阻塞IO，Blocking}
    \end{figure}

    \paragraph{2、非阻塞，Non-Blocking，NIO}~{}

    调用者调用某个函数时，并不会等待这个函数返回，而是每隔一段时间就去检查IO事件是否就绪，没有就绪时程序还可以执行其他操作。非阻塞IO的系统调用会立刻返回，一般可以根据errno区分IO事件有没有发生，例如，对于accept、recv、send，事件未发生时，errno通常被设置为EAGAIN。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.9]{Figures/非阻塞IO.png}
        \caption{非阻塞IO，Non-Blocking}
    \end{figure}

    \paragraph{3、IO多路复用，IO-Multiplexing}~{}

    Linux使用，select/poll/epoll函数，实现IO多路复用模型，这些函数虽然也会阻塞，但是一次性可以阻塞多个IO操作，可以同时对多个读写IO操作函数进行检测，直到有数据可读或者可写时，才真正的调用IO操作函数。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.9]{Figures/IO多路复用.png}
        \caption{IO多路复用，IO Multiplexing}
    \end{figure}

    \paragraph{4、信号驱动，Signal Driven}~{}

    套用Linux提供的接口进行，安装一个信号处理函数，然后进程继续运行不阻塞，当IO事件就绪时，进程会收到SIGIO信号，然后处理IO事件。

    内核在第一个阶段是异步，在第二个阶段是同步。信号驱动与非阻塞IO的区别在于，信号驱动的IO提供了消息通知的机制，不需要应用程序的进程不断地查询IO是否就绪，减少了系统API调用的次数，提高了效率。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.9]{Figures/信号驱动IO.png}
        \caption{信号驱动，Signal-Driven}
    \end{figure}

    \paragraph{5、异步，Asynchronous}~{}

    通过Linux的异步接口，例如aio\_read，告诉内核，文件描述符、缓冲区指针、缓冲区大小、文件偏移、通知方式，然后函数会立即返回，当内核完成向缓冲区拷贝数据之后，内核才会通知应用程序。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.9]{Figures/异步IO.png}
        \caption{异步IO，Asynchronous}
    \end{figure}

    \subsubsection{服务器编程的基本框架和事件处理模式}

    \begin{itemize}
        \item \textbf{IO处理单元}，处理客户连接，读写网络数据。
        \item \textbf{逻辑单元}，业务进程或者线程。
        \item \textbf{网络存储单元}，数据库，文件，缓存。
        \item \textbf{请求队列}，各个单元之间的通信方式。
    \end{itemize}

    IO处理单元与业务逻辑单元之间，使用请求队列进行通信。

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.5]{Figures/服务器编程基本框架.png}
        \caption{服务器编程基本框架}
    \end{figure}

    IO处理单元，需要等待并接受新的客户端连接，接受客户数据，并将服务器的响应数据返回给客户端。但是数据的收发并不一定都在IO处理单元中进行，也可能在逻辑单元中进行，具体取决于事件处理的模式。

    业务逻辑单元通常是一个进程或者一个线程，用于分析并处理客户的数据，然后将响应数据发送给IO处理单元或者直接发送给客户。服务器通常具有多个逻辑单元，进行多任务并发处理。

    请求队列，是各个单元之间通信方式的抽象，请求队列通常被实现为池的一部分。

    线程可以共享全局区的数据，进程间通信需要管道、内存映射等方式。

    Tomcat，Nginx。

    服务器通常需要处理三类事件：\textbf{IO事件、信号、定时事件}，同步IO模型通常用于实现Reactor模式，异步IO模型通常用于实现Proactor模式。

    \paragraph{Reactor模式}~{}

    主线程，即，IO处理单元，只负责监听文件描述符上是否有事件发生，如果有事件发生，则立即将该事件通知工作线程，即，逻辑处理单元，并将socket可读可写事件放入请求队列，交由工作线程来处理。

    主线程只负责进行监听，不做其他任何事情，数据读写、新链接的接受、业务逻辑处理，均在工作线程中完成。

    以epoll\_wait为例：

    \begin{enumerate}
        \item 主线程，向epoll内核事件表中，注册socket上的，读写就绪事件
        \item 主线程调用epoll\_wait，等待socket上有数据可读
        \item 当socket上，有数据可读时，epoll\_wait会通知主线程，主线程将socket可读事件加入请求队列
        \item 睡眠在请求队列上的某个线程被唤醒，从socket中读取数据，并处理客户请求，然后往epoll内核事件表中，注册该socket上的写就绪事件
        \item 主线程调用epll\_wait，等待socket可写
        \item 当socket可写时，epoll\_wait通知主线程，主线程将socket可写事件放入请求队列
        \item 睡眠在请求队列上的某个工作线程被唤醒，并向socket上写入服务器程序处理客户请求的结果
    \end{enumerate}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.6]{Figures/Reactor模式.png}
        \caption{Reactor模式}
    \end{figure}

    \paragraph{Proactor模式}~{}

    Proactor模式，将所有的读写操作都交给主线程和内核来进行处理，工作线程只负责业务逻辑。

    使用异步IO模型，aio\_read、aio\_write，来实现Proactor模式的工作流程是：

    \begin{enumerate}
        \item 主线程调用aio\_read函数，向内核注册socket读完成事件，同时一并告诉内核，缓冲区的位置，以及读操作完成时，通知应用程序的方式（以信号为例）
        \item 主线程继续处理其它逻辑
        \item 当socket中的数据，被读入缓冲区后，内核向应用程序发送一个信号，通知应用程序数据可用
        \item 应用程序预先定义好的信号处理函数，选择一个工作线程来处理客户的请求，处理完成后，调用aio\_write函数向内核注册socket上的写完成事件，并告诉内核写缓冲区的位置，以及写操作完成时，通知应用程序的方式
        \item 主线程继续处理其它逻辑
        \item 当缓冲区的数据被全部写入socket后，内核向应用程序发送信号，通知数据发送完毕
        \item 应用程序预先定义好的信号处理函数，选择一个工作线程来做善后工作，比如决定是否关闭socket
    \end{enumerate}

    \begin{figure}[H]
        \centering
        \includegraphics[scale=0.6]{Figures/Proactor模式.png}
        \caption{Proactor模式}
    \end{figure}

    \subsubsection{线程同步机制、线程池实现}

    线程池是服务器预先创建的一组子线程，线程池中的现场数量应该和CPU核心的数量保持一致，线程池中的子线程都运行着相同的代码。

    当出现新的任务时，主线程将通过某种算法，选择一个子线程，来处理任务。

    相比于动态创建线程，使用线程池的代价要小得多。

    线程池属于静态资源。

    多线程的应用程序，必须要考虑线程同步的问题和线程数据安全的问题，线程同步可以采用的策略有，\textbf{互斥锁、读写锁、条件变量、信号量，等}。

    相关代码在，WSL的/home/shiyuxin/CPPServer/CPPServer下。

    \paragraph{互斥锁类的编写}~{}

    可能需要手动安装，关于posix的文档，\textbf{sudo apt install manpages-posix-dev}。

    在构造函数中，我们需要初始化互斥锁，查看文档，\textbf{man pthread\_mutex\_init}，查看互斥锁的初始化函数，需要提供互斥锁的指针，和想要互斥锁被初始化成的属性，如果成功返回0。

    在析构函数中，我们需要销毁互斥锁，查看文档，\textbf{man pthread\_mutex\_destroy}，查看互斥锁的销毁函数，需要提供互斥锁的指针；如果成功销毁，函数返回0，如果失败，则可能返回，EAGAIN、ENOMEN、EPERM、EINVAL。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    \end{lstlisting}

    然后，我们需要实现给互斥锁上锁和解锁的功能，需要用到的函数时，pthread mutex lock和pthread mutex unkock，可以直接使用man命令查看文档。

    如果成功，这两个函数的返回值都是0，如果不成功，则会返回非零值，可能是EAGAIN、EINVAL、ENOTRECOVERABLE、EOWNERDEAD、EDEADLK、EBUSY、EPERM、EOWNERDEAD、EDEADLK，等。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
    \end{lstlisting}

    \paragraph{条件变量类的编写}~{}

    我们还需要编写一个条件变量类，来实现判断队列中是否有数据，如果无数据就让线程睡眠，这样的功能。

    我们自己编写一个类来实现上述功能，类的核心是一个pthread\_cond\_t类型的变量，在类的构造函数和析构函数中，我们需要初始化和销毁这个变量，涉及到的函数是pthread-cond-init和pthread-cond-destroy。如果初始化和销毁的操作成功，这两个函数会返回0，如果不成功，则会返回EAGAIN、ENOMEM，等。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    \end{lstlisting}

    然后我们需要封装条件变量的wait、timedwait、signal、broadcast，等功能，涉及到的函数是pthread-cond-wait、pthread-cond-timedwait、pthread-mutex-signal、pthread-cond-broadcast，可以通过man命令查看说明。

    如果成功调用，返回0，如果调用失败，返回与发生的错误对应的错误码。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
    \end{lstlisting}

    \paragraph{信号量类的编写}~{}

    信号量（semaphore）是一个sem\_t类型的变量，在头文件semaphore.h中，初始化和销毁需要用到的函数是sem-init和sem-destroy，如果初始化和销毁成功，返回0，如果失败，返回-1并设置相应的errno。

    sem-init函数需要提供pshared和value参数，如果pshared参数为0，表示信号量在当前进程的线程中共享，如果pshared不为0，表示信号量在进程直接共享，value则是信号量的初始值。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
    \end{lstlisting}

    接下来，需要实现信号量的两个重要的功能，wait和post，涉及到的函数分别是sem-wait和sem-post，可以使用man命令查看。同样的，操作成功时，函数返回0，操作失败时，函数返回-1，并设置对应的errno。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <semaphore.h>

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

int sem_post(sem_t *sem);
    \end{lstlisting}

    \paragraph{线程池类的编写}~{}

    首先，为了方便我们的代码，复用到各种不同类型的任务上，我们需要将线程池类写为模板类。

    线程池类的成员变量，应该包括：线程池中线程的数量、存储每个线程的数组、请求队列中最多允许等待的任务的数量、请求队列、互斥锁、信号量、用于判断是否需要结束一个线程的表质量。

    在构造函数中，我们需要初始化参数，并创建和分离每一个线程，涉及到的函数是pthread-create和pthread-detach。创建线程时，需要提供工作函数和工作函数的参数。分离线程是为了实现运行完毕的线程的资源回收。这两个函数在成功时，返回值都是0，在失败时，会返回相应的错误代码。

    对于工作函数，则要求是一个静态函数，返回值是void*，参数是void*。

    \begin{lstlisting}[language=c++, lineskip=3pt]
        #include <pthread.h>

        int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
        int pthread_detach(pthread_t thread);
    \end{lstlisting}

    在析构函数中，首先需要将停止标志量设置为true，使得线程池中的每个线程在检查到标志量为true后，停止运行。然后，需要将new出来的线程数组释放。

    然后，我们需要一个函数，能够让我们向线程池的请求队列中添加请求（add-request），在这个函数中，我们在操作请求队列时首先要进行加锁，因为线程池中的线程都可以对请求队列进行访问，枷锁完成后再向请求队列中添加新的请求，然后解锁，并让信号量发出post。

    然后，我们需要一个，针对每个线程的运行函数（run），在停止标志量为true之前，这个函数会不断地进行while循环，在while循环中，我们首先对信号量进行wait，等待有新的任务被添加。在等待到新的任务后，首先还是进行上锁，然后尝试从请求队列中取出有效的请求，取出请求后就可以解锁，让其他线程可以访问请求队列。最后，执行请求，请求执行完毕后，开始下一轮while循环。

    \subsubsection{Web服务器的实现}

    首先我们需要一个信号处理函数，编写信号处理函数，主要涉及到sigaction结构体，sigfillset函数，sigaction函数；并且，我们首先要对SIGPIPE信号进行处理，即处理客户端主动关闭连接的情况（项目中采用忽略的策略），如果不对SIGPIPE信号进行处理，在面对客户端主动关闭连接的场景时，会导致程序异常退出。

    相关的函数，可以使用man命令查看使用指南。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

// 定义一个用来处理信号的函数
void register_signal_handler(int signal, void(*handler)(int)) {
    struct sigaction signal_action; // 定义一个信号的处理器
    memset(&signal_action, 0, sizeof(signal_action)); // 先清空

    signal_action.sa_handler = handler; // 设置handler
    sigfillset(&signal_action.sa_mask); // 设置一个包含了所有信号的信号集，即阻塞所有的信号
    sigaction(signal, &signal_action, nullptr); // 注册信号处理
}

// （in main）处理SIGPIPE信号，即客户端主动关闭连接的情况，这里采用忽略SIGPIPE信号的策略
register_signal_handler(SIGPIPE, SIG_IGN);
    \end{lstlisting}

    建立一个TCP的Socket，并设置端口复用，然后将socket绑定到指定的IP和Port上，最后进行监听，涉及到的函数是，socket、setsockopt、bind、listen。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int socket(int domain, int type, int protocol);
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
    \end{lstlisting}

    下面实现，IO多路复用相关的代码，使用epoll进行。首先，创建一组struct epoll-event用于存储epoll事件，然后，调用epoll-create函数创建一个epoll实例，返回epoll文件描述符。

    \textbf{epoll one-shot事件}，epoll默认为水平触发，即使改为边沿触发，仍然可能会出现，某一个socket上同一个事件被多次触发的情况，在并行程序中，就会造成混乱。注册EPOLLONESHOT事件可以解决这个问题，当一个文件描述符注册了EPOLLONESHOT事件之后，操作系统最多触发其上注册的一个可读、可写或者异常事件，而且只触发一次。如果想要能被再次触发，就需要重新注册事件。

    然后，需要实现向epoll中，添加和删除文件描述符的代码，涉及到的函数是epoll-ctl，成功返回0，失败返回-1并设置相应的errno，可以使用man命令查看。

    使用epoll时，需要将文件描述符，设置为非阻塞的状态，

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    \end{lstlisting}

    主线程中，需要一个while循环，不断的等待epoll事件，涉及到的是epoll-wait函数。

    epoll-wait调用成功以后，我们需要遍历事件数组，处理每一个事件。如果某个事件对应的文件描述符，和我们用于监听的socket的文件描述符相同，则说明有客户端正在请求进行连接。

    对于一个客户端的连接请求，首先我们需要建立连接，涉及到的函数是accept。

    这里要做一个处理，如果服务器已经建立的连接的数量，大于了我们预设的最多的文件描述符数量，则我们需要返回一个“服务器正忙”，然后调用close函数，关闭客户端的连接。

    成功与客户端建立起连接后，就可以将客户端连接纳入我们自定义的HttpConnection对象的数组，并初始化客户端socket的文件描述符、地址，并设置客户端socket的复用，然后将客户端socket添加进epoll，等待读事件。

    如果出现了读事件，则主线程负责一次性读取所有的请求数据，然后，向线程池中添加任务，由线程池中的子线程负责处理请求，生成响应。响应生成完毕后，子线程向epoll注册写就绪事件，等待主线程写响应数据。

    主线程收到epoll的写就绪事件后，向客户端写响应数据，写完毕后，关闭socket连接，一次HTTP请求处理完成。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    \end{lstlisting}

    对于客户端HTTP请求信息的读取，首先我们需要定义读缓冲区，然后，从socket中读取数据，需要使用的系统函数是recv，可以使用man命令查看说明。

    如果recv返回-1，说明有错误出现，如果出现的错误是EAGAIN或者EWOULDBLOCK，说明socket中暂时无数据可读（由于使用了epoll，socket都被设置成非阻塞的）。

    如果recv返回0，说明客户端断开了连接。

    如果读取成功，recv会返回本次读取到的字节数。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
    \end{lstlisting}

    读取到数据后，接下来，是对客户端HTTP请求的解析。

    首先，为了解析HTTP我们需要使用有限状态机的思想，利用C++中的枚举类型，定义处理HTTP请求过程中的各种中间状态。

    然后，编写相应的函数，解析客户端HTTP请求的首行、头部、主体，如果遇到不完整的数据，说明客户端请求没有被完全读取到，这似乎后需要重新注册该客户端连接上的EPOLLIN事件，继续读请求数据。

    \begin{lstlisting}[language=C++, lineskip=3pt]
// HTTP请求方法，这里只支持GET
enum METHOD {
    GET = 0, 
    POST, 
    HEAD, 
    PUT, 
    DELETE, 
    TRACE, 
    OPTIONS, 
    CONNECT
};

/**
    * 解析客户端请求时，主状态机的状态
    */
enum CHECK_STATE { 
    CHECK_STATE_REQUESTLINE = 0, // CHECK_STATE_REQUESTLINE：当前正在分析请求行
    CHECK_STATE_HEADER,          // CHECK_STATE_HEADER：当前正在分析头部字段
    CHECK_STATE_CONTENT          // CHECK_STATE_CONTENT：当前正在解析请求体
};

/**
    * 从状态机的三种可能状态，即行的读取状态，分别表示
    */
enum LINE_STATUS { 
    LINE_OK = 0, // 1.读取到一个完整的行 
    LINE_BAD,    // 2.行出错 
    LINE_OPEN    // 3.行数据尚且不完整
};

/**
    * 服务器处理HTTP请求的可能结果，报文解析的结果
    */
enum HTTP_CODE { 
    NO_REQUEST,         // 请求不完整，需要继续读取客户数据
    GET_REQUEST,        // 表示获得了一个完成的客户请求
    BAD_REQUEST,        // 表示客户请求语法错误
    NO_RESOURCE,        // 表示服务器没有资源
    FORBIDDEN_REQUEST,  // 表示客户对资源没有足够的访问权限
    FILE_REQUEST,       // 文件请求，获取文件成功
    INTERNAL_ERROR,     // 表示服务器内部错误
    CLOSED_CONNECTION   // 表示客户端已经关闭连接了
};
    \end{lstlisting}

    创建内存映射，和，取消内存映射，需要使用到mmap和munmap函数，

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
    \end{lstlisting}

    在写响应数据的时候，由于，响应首行以及头部，和，响应主体，是两个不同的部分，所以需要用到分散写函数，writev，可以使用man命令查看使用说明。

    \begin{lstlisting}[language=C++, lineskip=3pt]
#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
    \end{lstlisting}

    \clearpage
    \bibliography{References}
    \bibliographystyle{unsrt}
    
\end{document}
